/*
Copyright 2025 Codenotary Inc. All rights reserved.

SPDX-License-Identifier: BUSL-1.1
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://mariadb.com/bsl11/

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package document

import (
	"crypto/rand"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"io"
	"sync/atomic"
	"time"
)

/*
	DocumentID is a 16-byte identifier that is automatically generated
	by the server upon document insertion if not provided. The 16-byte DocumentID is composed of:

		1) A 4-byte timestamp value, representing the time the document was created,
		measured in seconds since the Unix epoch.
		2) A 8-byte value, initialized to the previous transaction id.
		3) A 4-byte incremental counter value, generated using a secure random number generator.

	The timestamp portion of the DocumentID allows documents to be sorted by creation time, which
	can be useful for certain types of queries. The tx id portions of the DocumentID ensure that
	the identifier is unique across all documents in a collection, even if multiple documents
	are inserted in the same second. The 4-byte value ensures randomness.
*/

// GeneratedDocIDLength is the length of a DocumentID.
const GeneratedDocIDLength = 16

const MaxDocumentIDLength = 32

// DocumentID is an identifier that is automatically generated by the server upon document insertion if not provided.
type DocumentID []byte

// documentIDCounter is a counter used to generate unique monotically incremental number for the document id.
var documentIDCounter = getRandUint32()

// NewDocumentIDFromTx generates a new DocumentID.
func NewDocumentIDFromTx(txID uint64) DocumentID {
	return NewDocumentIDFromTimestamp(time.Now(), txID)
}

// NewDocumentIDFromTimestamp generates a new DocumentID from a timestamp.
func NewDocumentIDFromTimestamp(timestamp time.Time, txID uint64) DocumentID {
	var b [GeneratedDocIDLength]byte

	// The first 4 bytes are the timestamp.
	binary.BigEndian.PutUint32(b[0:4], uint32(timestamp.Unix()))

	// The next 8 bytes are the precommitted transaction id.
	binary.BigEndian.PutUint64(b[4:12], txID)

	// The next 4 bytes are the monotically increasing number.
	counter := atomic.AddUint32(&documentIDCounter, 1)
	binary.BigEndian.PutUint32(b[12:16], counter)

	return b[:]
}

// NewDocumentIDFromRawBytes generates a new DocumentID from a byte slice.
func NewDocumentIDFromRawBytes(b []byte) (DocumentID, error) {
	if len(b) == 0 {
		return nil, ErrIllegalArguments
	}
	if len(b) > MaxDocumentIDLength {
		return nil, ErrMaxLengthExceeded
	}

	id := make([]byte, len(b))
	copy(id, b)

	return id, nil
}

// NewDocumentIDFromHexEncodedString returns a DocumentID from a hex string.
func NewDocumentIDFromHexEncodedString(hexEncodedDocID string) (DocumentID, error) {
	decodedLen := hex.DecodedLen(len(hexEncodedDocID))

	buf := make([]byte, decodedLen)

	_, err := hex.Decode(buf, []byte(hexEncodedDocID))
	if err != nil {
		return nil, err
	}

	return NewDocumentIDFromRawBytes(buf)
}

func getRandUint32() uint32 {
	var b [4]byte

	_, err := io.ReadFull(rand.Reader, b[:])
	if err != nil {
		panic(fmt.Errorf("cannot initialize document id rand reader: %v", err))
	}

	return binary.BigEndian.Uint32(b[:])
}

// EncodeToHexString returns the hex representation of the DocumentID.
func (id DocumentID) EncodeToHexString() string {
	return hex.EncodeToString(id)
}

// Timestamp returns the timestamp portion of the DocumentID.
func (id DocumentID) Timestamp() time.Time {
	unixSecs := binary.BigEndian.Uint32(id[:])
	return time.Unix(int64(unixSecs), 0).UTC()
}
